package xyz.hubery.service.impl;

import cn.hutool.bloomfilter.BitMapBloomFilter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import xyz.hubery.config.RedissonLock;
import xyz.hubery.enums.CodeEnum;
import xyz.hubery.mapper.SeckillGoodsMapper;
import xyz.hubery.model.BusException;
import xyz.hubery.model.Constants;
import xyz.hubery.pojo.CartGoods;
import xyz.hubery.pojo.Orders;
import xyz.hubery.pojo.SeckillGoods;
import xyz.hubery.pojo.StockRecord;
import xyz.hubery.service.SeckillService;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@DubboService
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private BitMapBloomFilter bloomFilter;
    @Autowired
    private RedissonLock redissonLock;

//    /**
//     * 先将缓存中的库存数据同步到mysql（此时需要检查版本是否一致,及数据库该数据是否被修改过）
//     * 每分钟查询一次数据库，更新redis中的秒杀商品数据
//     * 条件为startTime < 当前时间 < endTime，库存大于0
//     */
//    @Scheduled(fixedRate = 5000)  // 60000毫秒 = 1分钟
//    public void refreshRedis() {
//        log.info("将redis中秒杀商品的库存数据同步到mysql ");
//        // 将redis中秒杀商品的库存数据同步到mysql
//        List<SeckillGoods> seckillGoodsListOld = redisTemplate.boundHashOps("seckillGoods").values();
//        for (SeckillGoods seckillGoods : seckillGoodsListOld) {
//            // 在数据库查询秒杀商品
//            SeckillGoods sqlSeckillGoods = seckillGoodsMapper.selectById(seckillGoods.getId());
//            if (sqlSeckillGoods != null && sqlSeckillGoods.getVersion().equals(seckillGoods.getVersion())) {
//                // 使用乐观锁更新，同时更新版本号
//                boolean success = seckillGoodsMapper.update(
//                        new LambdaUpdateWrapper<SeckillGoods>()
//                                .eq(SeckillGoods::getId, seckillGoods.getId())
//                                .eq(SeckillGoods::getVersion, sqlSeckillGoods.getVersion()) // 版本一致才更新
//                                .set(SeckillGoods::getStockCount, seckillGoods.getStockCount())
//                                .set(SeckillGoods::getVersion, sqlSeckillGoods.getVersion() + 1)
//                ) > 0;
//                if (!success) {
//                    log.warn("数据回写失败，版本号已变更，商品ID: {}", seckillGoods.getId());
//                }
//            } else {
//                log.warn("数据版本不一致，跳过回写。商品ID: {}，Redis版本: {}，DB版本: {}",
//                        seckillGoods.getId(), seckillGoods.getVersion(), sqlSeckillGoods.getVersion());
//            }
//        }
//
//
//        log.info("同步mysql秒杀商品到redis...");
//        // 1.查询数据库中正在秒杀的商品
//        QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper<>();
//        Date date = new Date();
//        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
//        queryWrapper.le("startTime", now) // 当前时间晚于开始时间
//                .ge("endTime", now) // 当前时间早于开始时间
//                .gt("stockCount", 0); // 库存大于0
//
//        List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(queryWrapper);
//
//
//        // 2.删除之前的秒杀商品
//        redisTemplate.delete("seckillGoods");
//
//
//        // 3.保存现在正在秒杀的商品
//        for (
//                SeckillGoods seckillGoods : seckillGoodsList) {
//            redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(), seckillGoods);
//        }
//
//    }

    /**
     * 定时任务，定时扫描正在进行中的商品，将其添加到缓存中（预热缓存）
     * 如果缓存中没有，则添加，否则不添加（防止覆盖缓存中的库存信息）
     */
    @Scheduled(fixedRate = 15000) // 每15秒执行一次
    public void refreshRedisByPreheat() {
        log.info("预热缓存...");
        // 1.查询数据库中正在秒杀的商品
        List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(
                new LambdaQueryWrapper<SeckillGoods>()
                        .gt(SeckillGoods::getStockCount, 0)
                        .le(SeckillGoods::getStartTime, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
                        .ge(SeckillGoods::getEndTime, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
        );

        redisTemplate.delete("seckillGoodsIdSet");
        // 3.保存现在正在秒杀的商品
        for (SeckillGoods seckillGoods : seckillGoodsList) {
            // 计算过期时间
            long expireTime = seckillGoods.getEndTime().getTime() - System.currentTimeMillis();
            // 缓存中没有，则添加（为每个商品单独存储，便于设置过期时间）
            redisTemplate.opsForValue().setIfAbsent("seckillGoods:" + seckillGoods.getGoodsId(), seckillGoods, expireTime, TimeUnit.MILLISECONDS);
            // 单独存储商品id与库存(注意这里库存数据不要设置过期时间，因为库存信息是需要会写到数据库中的，如果过期了我们就获取不到库存信息了，但商品信息过期时，我们就删除库存并且回写数据库)
            // TODO 这里需要特判，如果库存数据丢失，应该从库存记录中计算得到，这里先不处理
            redisTemplate.opsForValue().setIfAbsent("seckillStock:" + seckillGoods.getGoodsId(), seckillGoods.getStockCount());
            // 2.存储正在进行秒杀的商品id
            redisTemplate.opsForSet().add("seckillGoodsIdSet", seckillGoods.getGoodsId());
            // 将正在秒杀的商品保存到布隆过滤器
            bloomFilter.add(seckillGoods.getGoodsId().toString());
        }
    }

    /**
     * 前台用户查询秒杀商品
     *
     * @param page 页数
     * @param size 每页条数
     * @return 查询结果
     */
    @Override
    public Page<SeckillGoods> findPageByRedis(int page, int size) {

        // 1、从缓存中获取所有的秒杀商品 TODO 优化
        // 1.1 获取秒杀商品id列表
        Set<Long> seckillGoodsIdSet = redisTemplate.opsForSet().members("seckillGoodsIdSet");
        List<SeckillGoods> seckillGoodsList = new ArrayList<>();
        assert seckillGoodsIdSet != null;
        if (!seckillGoodsIdSet.isEmpty()) {
            for (Long goodsId : seckillGoodsIdSet) {
                SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("seckillGoods:" + goodsId);
                // 查询库存
                Integer stockCount = (Integer) redisTemplate.opsForValue().get("seckillStock:" + goodsId);
                assert seckillGoods != null;
                seckillGoods.setStockCount(stockCount);
                seckillGoodsList.add(seckillGoods);
            }
        }

        // 2.获取当前页商品列表
        // 开始截取索引
        int start = (page - 1) * size;
        // 结束截取索引
        int end = Math.min(start + size, seckillGoodsList.size());
        // 获取当前页结果集
        List<SeckillGoods> seckillGoods = seckillGoodsList.subList(start, end);

        // 3.构造页面对象
        Page<SeckillGoods> seckillGoodsPage = new Page<>();
        seckillGoodsPage.setCurrent(page) // 当前页
                .setSize(size) // 每页条数
                .setTotal(seckillGoodsList.size()) // 总条数
                .setRecords(seckillGoods); // 结果集
        return seckillGoodsPage;
    }


    @Override
    public SeckillGoods findSeckillGoodsByRedis(Long id) {
        // 布隆过滤器判断秒杀商品是否存在，如果不存在，直接返回空
        if (!bloomFilter.contains(id.toString())){
            log.info("布隆过滤器判断商品不存在！,商品id：{}", id);
            return null;
        }

        // 先查基础信息
        SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.opsForValue().get("seckillGoods:" + id);
        if (seckillGoods == null) { // 要么缓存失效要么过期了, 这里需要去查询数据库，如果在秒杀中，则返回商品信息并且加入缓存，否则返回404
            SeckillGoods dbSeckillGoods = seckillGoodsMapper.selectOne(new QueryWrapper<SeckillGoods>().eq("goodsId", id));
            if (dbSeckillGoods != null ) {
                // 判断是否在秒杀中
                long now = System.currentTimeMillis();
                if( dbSeckillGoods.getStartTime().getTime() <= now && dbSeckillGoods.getEndTime().getTime() >= now) { // 正在秒杀
                    seckillGoods = dbSeckillGoods;
                    redisTemplate.opsForValue().set("seckillGoods:" + id, seckillGoods);
                    bloomFilter.add(seckillGoods.getGoodsId().toString());
                }else{
                    throw new BusException(CodeEnum.GOODS_NOT_FOUND);
                }
            }else{
                throw new BusException(CodeEnum.GOODS_NOT_FOUND);
            }
        }
        // 再查库存
        Integer stockCount = (Integer) redisTemplate.opsForValue().get("seckillStock:" + id);
        seckillGoods.setStockCount(stockCount);
        return seckillGoods;
    }


    // TODO 定期扫描库存，如果库存为0，则删除缓存中的库存信息，结束秒杀

    // TODO 该段代码目前有以下几个问题
    // 1、并发更新造成库存错误
    // 2、缓存中的库存更新后，定时任务执行，数据库中的原库存覆盖缓存中的库存
    // 3、订单过期后需要回滚库存
    @Override
    public Orders createOrder(Orders orders) {
        String lockKey = orders.getCartGoods().get(0).getGoodId().toString();
        if (redissonLock.lock(lockKey,10000)){
            try {
                // 1.生成订单对象
                orders.setId(IdWorker.getIdStr()); // 手动生成订单id
                orders.setStatus(1); // 订单状态未付款
                orders.setCreateTime(new Date()); // 订单创建时间
                orders.setExpire(new Date(new Date().getTime() + 1000 * 60 * 5)); // 订单过期时间
                // 计算商品价格
                CartGoods cartGoods = orders.getCartGoods().get(0);
                Integer num = cartGoods.getNum();
                BigDecimal price = cartGoods.getPrice();
                BigDecimal sum = price.multiply(BigDecimal.valueOf(num));
                orders.setPayment(sum);

                // 2.减少秒杀商品库存
                // 查询秒杀商品
                SeckillGoods seckillGoods = findSeckillGoodsByRedis(cartGoods.getGoodId());
                // TODO 减少库存，这里需要完善成原子级操作，这里先简单处理（判断和减库存需要原子级操作）
                // 查询库存，库存不足抛出异常
                Integer stockCount = (Integer) redisTemplate.opsForValue().get("seckillStock:" + cartGoods.getGoodId());
                if (stockCount < num) {
                    throw new BusException(CodeEnum.NO_STOCK_ERROR);
                }
                redisTemplate.opsForValue().set("seckillStock:" + cartGoods.getGoodId(), stockCount - num);


                // 3.保存订单数据
                redisTemplate.setKeySerializer(new StringRedisSerializer());
                // 设置订单一分钟过期
                redisTemplate.opsForValue().set("orders:" + orders.getId(), orders, 1, TimeUnit.MINUTES);

                /**
                 * 给订单创建副本，副本的过期时间长于原订单
                 * redis过期后触发过期事件时，redis数据已经过期，此时只能拿到key，拿不到value。
                 * 而过期事件需要回退商品库存，必须拿到value即订单详情，才能拿到商品数据，进行回退操作
                 * 我们保存一个订单副本，过期时间长于原订单，此时就可以通过副本拿到原订单数据
                 */
                redisTemplate.opsForValue().set("orders:" + orders.getId() + "_copy", orders, 2, TimeUnit.MINUTES);


                // 4.生成一条库存操作记录，用于后续核对信息
                StockRecord stockRecord = new StockRecord();
                stockRecord.setOrderId(orders.getId());
                stockRecord.setGoodsId(seckillGoods.getGoodsId());
                stockRecord.setChangeQuantity(-1 * num);
                stockRecord.setOperationType(1);
                stockRecord.setOperationTime(new Date());
                stockRecord.setOperator("user");
                stockRecord.setRemark("用户下单预扣库存");
                rocketMQTemplate.syncSend(Constants.SYNC_STOCK_OPERATION_TOPIC, stockRecord);
                return orders;
            }finally {
                redissonLock.unlock(lockKey);
            }
        }else{
            return null;
        }

    }


    @Override
    public Orders findOrder(String id) {
        return (Orders) redisTemplate.opsForValue().get("orders:" + id);
    }


    @Override
    public Orders pay(String orderId) {
        // 1.查询订单，设置数据
        Orders orders = this.findOrder(orderId);
        if (orders == null || orders.getStatus() != 1) {
            throw new BusException(CodeEnum.ORDER_EXPIRED_ERROR); // 订单过期
        }

        orders.setStatus(2);
        orders.setPaymentTime(new Date());
        orders.setPaymentType(2); // 支付宝支付

        // 2.从redis删除订单数据
        redisTemplate.delete("orders:" + orderId);
        redisTemplate.delete("orders:" + orderId + "_copy");

        // 3.返回订单数据
        return orders;
    }


    @Override
    public void updateStock(long goodId, int num, boolean updateAll) {
        // 基于乐观锁 实现 秒杀商品库存更新
        // 1.查询商品数据
        SeckillGoods seckillGoods = seckillGoodsMapper.selectOne(new LambdaQueryWrapper<SeckillGoods>()
                .eq(SeckillGoods::getGoodsId, goodId));
        if (seckillGoods == null) {
            throw new BusException(CodeEnum.GOODS_NOT_FOUND);
        }
        if (!updateAll) { // 是否更新所有库存
            num += seckillGoods.getStockCount();
        }
        // 2.更新库存
        seckillGoodsMapper.update(new LambdaUpdateWrapper<SeckillGoods>()
                .eq(SeckillGoods::getId, seckillGoods.getId())
                .eq(SeckillGoods::getVersion, seckillGoods.getVersion())
                .set(SeckillGoods::getStockCount, num)
                .set(SeckillGoods::getVersion, seckillGoods.getVersion() + 1)
        );
    }
}
